using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using ExtensionMethods;
using Lap.Collections;
using Lap.Extensions.Core;
using PpSmsSender.Db;
using PrivateReflectionUsingDynamic;

namespace PpSmsSender
{
    public class WebServer : IDisposable
    {
        #region Delegates
        public delegate string ProcessRequest(HttpListenerContext context, HttpListenerRequest request);
        #endregion

        private static IDbSmsSender _dbSmsSender;

        private HttpListener _listener;
        private readonly string pass;
        private readonly string user;
        private ProcessRequest _requestHandler;
        public volatile bool WebDied;
        private const string DoctypeHTML = "<!DOCTYPE html >\r\n";

        public WebServer(IDbSmsSender dbSmsSender)
        {
            _dbSmsSender = dbSmsSender;
            this.user = ConfigurationManager.AppSettings["httpAuth"].ConfItem("User Id");
            this.pass = ConfigurationManager.AppSettings["httpAuth"].ConfItem("Password");
        }

        #region Setup listener
        public void Start(string uriPrefix, ProcessRequest requestHandler)
        {
            this._listener = new HttpListener();
            this._requestHandler = requestHandler;
            this._listener.Prefixes.Add(uriPrefix);
            this._listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous | AuthenticationSchemes.Basic;

            this._listener.IgnoreWriteExceptions = true;
            this._listener.Start();
            this._listener.BeginGetContext(this.GetContextCallback, null);
        }

        public void Stop()
        {
            try
            {
                if (_listener != null)
                    this._listener.Stop();
            }
            catch
            {
                if (_listener != null)
                    this._listener.AsExposed().RemoveAll(false);
            }
            this._listener = null;
        }

        public void GetContextCallback(IAsyncResult result)
        {
            HttpListenerContext context;
            try
            {
                context = this._listener.EndGetContext(result);
            }
            catch (Exception)
            {
                this.WebDied = true;
                if (_listener != null)
                    this._listener.AsExposed().RemoveAll(true);
                return;
            }
            this._listener.BeginGetContext(this.GetContextCallback, null);
            this.ProcessContext(context);
        }

        private void ProcessContext(HttpListenerContext context)
        {
            HttpListenerRequest request = context.Request;
            HttpListenerResponse response = context.Response;
            response.ContentEncoding = Encoding.UTF8;

            string responseString = this._requestHandler(context, request);
            
            var buffer = Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;

            using (Stream outputStream = response.OutputStream)
            {
                outputStream.Write(buffer, 0, buffer.Length);
            }
            try
            {
                response.Close();
            }
            catch (Exception)
            {
            }
        }
        #endregion

        public string Webhandler(HttpListenerContext context, HttpListenerRequest request)
        {
            HttpListenerBasicIdentity identity =
                context.User != null && (context.User.Identity as HttpListenerBasicIdentity) != null
                ? context.User.Identity as HttpListenerBasicIdentity
                : new HttpListenerBasicIdentity("" + request.QueryString["username"], "" + request.QueryString["password"]);

            if (identity.Name != this.user || identity.Password != this.pass)
            {
                context.Response.StatusCode = 401;
                context.Response.AddHeader("WWW-Authenticate", "Basic Realm=\"RatatoskSMS\""); // show login dialog
                return "Please authenticate";
            }

            try
            {
                StringBuilder result = new StringBuilder();
                string lowerUrl = request.Url.AbsolutePath.ToLowerInvariant();
                if (lowerUrl.EndsWith("/status"))
                {
                    context.Response.Headers["Expires"] = "Fri, 30 Oct 1998 14:19:41 GMT";
                    return StatusReport();
                }

                if (lowerUrl.EndsWith("/checkstatus"))
                {
                    context.Response.Headers["Expires"] = "Fri, 30 Oct 1998 14:19:41 GMT";
                    int queueLength;
                    int pausedLength;
                    using (SmsDataContext db = new SmsDataContext())
                    {
                        queueLength = db.SmsMessages.Count(m => m.Typ == (int)SmsMessage.Types.ToSend) + _dbSmsSender.Sender.SendQueueLength;
                        pausedLength = db.SmsMessages.Count(m => m.Typ == (int)SmsMessage.Types.ToSend && m.EarliestSend > DateTime.Now);
                    }
                    int free = _dbSmsSender.Config.LeftToSendInPeriod;
                    int prio = request.QueryString["prio"].Parse(15);
                    int reserve = _dbSmsSender.Config.Priorities[prio].Reserve;
                    return free + "/" + queueLength + "/" + pausedLength + "/" + reserve;
                }

                if (lowerUrl.EndsWith("/sendsms"))
                {
                    return this.SendSms(context, request, result);
                }

                if (lowerUrl.Contains("/admin/showlog"))
                {
                    string content;
                    try
                    {
                        string filename = request.Url.AbsolutePath.Substring(lowerUrl.IndexOf("/admin/showlog", StringComparison.Ordinal) + "/admin/showlog".Length).Trim();
                        content = Tail(filename, 50000);

                    }
                    catch (Exception ex)
                    {

                        content = "Failed to read file:" + ex.Message;

                    }
                    return DoctypeHTML
                           + ("".HtmlTag("head") + content.HtmlTag("pre").HtmlTag("div").HtmlTag("body")).HtmlTag("html");
                }

                if (lowerUrl.Contains("/admin/"))
                {
                    string output = "";
                    string command = request.Url.AbsolutePath.Substring(lowerUrl.IndexOf("/admin/", StringComparison.Ordinal) + "/admin/".Length);
                    command = command.Replace("/", " ");
                    try
                    {
                        _dbSmsSender.ExecAdminCommand(command, s => output += s + "<br />");

                    }
                    catch (Exception ex)
                    {
                        output = "Command failed:" + ex.Message + "\r\n" + ex.StackTrace;
                    }

                    return DoctypeHTML
                           + ("".HtmlTag("head") + output.HtmlTag("pre").HtmlTag("div").HtmlTag("body")).HtmlTag("html");
                }

                if (lowerUrl.EndsWith("/ack"))
                {
                    return this.RecieveSendAckCallback(context, request, result);
                }
                if (lowerUrl.EndsWith("/rec"))
                {
                    return this.RecieveRecievedMsgCallback(context, request, result);
                }
            }
            catch (Exception ex2)
            {

                return "Failed to execute command: " + ex2.Message;
            }

            context.Response.StatusCode = 404;
            return "Unknown page";
        }


        private static string Tail(string filename, int Length)
        {
            StreamReader cmdLogReader = new StreamReader(new FileStream(EnvironmentUtils.RootPath("./" + filename + ".log"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite), true);

            string content;
            if (cmdLogReader.BaseStream.Length < (Length - 1))
            {
                content = cmdLogReader.ReadToEnd();
            }
            else
            {
                cmdLogReader.BaseStream.Seek(-Length, System.IO.SeekOrigin.End);
                cmdLogReader.ReadLine();
                content = cmdLogReader.ReadToEnd();
            }
            return content;
        }

        private string RecieveRecievedMsgCallback(HttpListenerContext context, HttpListenerRequest request, StringBuilder result)
        {
            var enc = Encoding.UTF8;
            var form = DecodeForm(request, enc);
            int cnt = form["msgId"].Count;

            for (int ix = 0; ix < cnt; ++ix)
            {
                string msgid = form["msgId"][ix];
                string fromnumber = form["fromnumber"][ix];
                string msg = form["msg"][ix];
                string time = form["time"][ix];
                string bynumber = form["bynumber"][ix];
                string callbackid = form["callbackid"][ix];
                _dbSmsSender.Sender.OutputLn(string.Format("Recieved Msg callback: {0} {1} \"{2}\" {3} {4} {5}",
                                                           msgid,
                                                           fromnumber,
                                                           msg,
                                                           time,
                                                           bynumber,
                                                           callbackid
                                                 ));
            }
            return "";
        }

        private string RecieveSendAckCallback(HttpListenerContext context, HttpListenerRequest request, StringBuilder result)
        {
            var enc = Encoding.UTF8;
            var form = DecodeForm(request, enc);
            int cnt = form["msgId"].Count;

            for (int ix = 0; ix < cnt; ++ix)
            {
                string msgid = form["msgId"][ix];
                string callbackid = form["callbackid"][ix];
                string time = form["time"][ix];
                string status = form["status"][ix];
                string tonumber = form["tonumber"][ix];
                string bynumber = form["bynumber"][ix];
                _dbSmsSender.Sender.OutputLn(string.Format("Recieved Ack callback: {0} {1} {2} {3} {4} {5}",
                                                           msgid,
                                                           callbackid,
                                                           time,
                                                           status,
                                                           tonumber,
                                                           bynumber
                                                 ));
            }
            return "";
        }

        private static DictionaryWithDefault<string, List<string>> DecodeForm(HttpListenerRequest request, Encoding enc)
        {
            string formdata = "" + GetRequestPostData(request);
            var form = new DictionaryWithDefault<string, List<string>>(k => new List<string>());
            formdata.Split('&').ForEach(e =>
                                            {
                                                var esplit = (e + "==").Split('=');
                                                form[esplit[0]].Add(esplit[1].UrlDecode(enc));
                                            });
            return form;
        }

        public static string StatusReport()
        {
            int queueLength;
            int pausedLength;
            using (SmsDataContext db = new SmsDataContext())
            {
                queueLength = db.SmsMessages.Count(m => m.Typ == (int)SmsMessage.Types.ToSend) + _dbSmsSender.Sender.SendQueueLength;
                pausedLength = db.SmsMessages.Count(m => m.Typ == (int)SmsMessage.Types.ToSend && m.EarliestSend > DateTime.Now);
            }

            string total = "";
            string table =
                (
                    ("Send Queue length").HtmlTag("td", "colspan=3")
                    + ("" + queueLength).HtmlTag("td", "colspan=3")
                ).HtmlTag("tr") +
                (
                    ("out of wich is paused").HtmlTag("td", "colspan=3")
                    + ("" + pausedLength).HtmlTag("td", "colspan=3")
                ).HtmlTag("tr") +
                (
                    ("Messages left for the period").HtmlTag("td", "colspan=3")
                    + ("" + _dbSmsSender.Config.LeftToSendInPeriod).HtmlTag("td", "colspan=3")
                ).HtmlTag("tr");


            string row = "";
            row += ("Seq#").HtmlTag("td");
            row += ("Port").HtmlTag("td");
            row += ("Sent total").HtmlTag("td");
            row += ("Send Queue Length").HtmlTag("td");
            row += ("Send/Recieve").HtmlTag("td");
            table += row.HtmlTag("tr");


            _dbSmsSender.Sender.Modems.Values
                .OrderBy(m => m.Config.ModemSeq)
                .ForEach(
                    m =>
                    {
                        row = "";
                        row += ("#" + m.Config.ModemSeq).HtmlTag("td");
                        row += (m.ModemHandler.Name).HtmlTag("td");
                        row += ("" + m.ModemHandler.SentMessageCount).HtmlTag("td");
                        row += ("" + m.ModemHandler.SendQueueLength).HtmlTag("td");
                        row += (m.ModemHandler.IsReciveOnly ? "Only Recieve" : "Send and Recieve").HtmlTag("td");
                        table += row.HtmlTag("tr");
                    });

            return DoctypeHTML + ("".HtmlTag("head") + table.HtmlTag("table", "border='1'").HtmlTag("body")).HtmlTag("html");
        }

        public string SendSms(HttpListenerContext context, HttpListenerRequest request, StringBuilder result)
        {
            try
            {
                var path = request.Url.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
                NameValueCollection queryString = request.QueryString;
                Encoding enc = Encoding.GetEncoding(request.QueryString["charset"] ?? "utf-8");
                NameValueCollection queryStringDecoded = HttpUtility.ParseQueryString(context.Request.Url.Query, enc);

                string to = queryStringDecoded["to"];
                string text = "" + queryStringDecoded["text"];
                string smsc = "" + queryString["smsc"];
                string prio = "" + queryString["prio"];
                string callbackid = "" + queryString["callbackid"];
                string batchid = "" + queryString["batchid"];

                SmsMessage sms = new SmsMessage { Text = text, Number = to };
                if (sms.Number.IsEmpty() || sms.Number.Category != PhoneNumber.NumberCategory.Mobile)
                {
                    context.Response.StatusCode = 500;
                    context.Response.StatusDescription = "Invalid phone number " + to;
                    return "Invalid phone number " + to;
                }

                if (text.Trim() == "")
                {
                    context.Response.StatusCode = 500;
                    context.Response.StatusDescription = "Empty Text is not allowed.";
                    return "Empty Text is not allowed";
                }

                decimal modemNo;
                if ((modemNo = smsc.Replace("modem", "").Parse(-1m)) > 0)
                {
                    PhoneNumber searchPhone = PhoneNumber.CreateLocal("" + modemNo);

                    PhoneNumber phone = _dbSmsSender.Sender.Modems.Values
                        .Where(m => m.Config.ModemSeq == modemNo || m.Config.PhoneNumber == "" + searchPhone)
                        .Select(m => m.Config.PhoneNumber)
                        .FirstOrDefault();
                    if (phone != null)
                    {
                        sms.RecievedBy = phone;
                        result.AppendLine("Will try to send by modem #" + phone);
                    }
                }
                sms.Priority = prio.Parse(-1);
                if (sms.Priority > 15)
                    sms.Priority = 15;
                if (sms.Priority < 0)
                    sms.Priority = 8;
                sms.Callbackid = callbackid;
                sms.Batchid = batchid;
                //insert in db
                using (SmsDataContext db = new SmsDataContext())
                {
                    sms.InsertOrAttach(db);
                    sms.Type = SmsMessage.Types.ToSend;
                    db.SaveChanges();
                }

                if (sms.Priority < 8)
                {
                    _dbSmsSender.WakeupMainthread();
                }
                return result.ToString();
            }
            catch (Exception ex)
            {
                context.Response.StatusCode = 500;
                context.Response.StatusDescription = "Program error: " + ex.Message;
                return "Program error: " + ex.Message;
            }
        }

        public static string GetRequestPostData(HttpListenerRequest request)
        {
            if (!request.HasEntityBody)
            {
                return null;
            }
            using (Stream body = request.InputStream) // here we have data
            {
                using (StreamReader reader = new StreamReader(body, request.ContentEncoding))
                {
                    return reader.ReadToEnd();
                }
            }
        }

        #region Implementation of IDisposable
        /// <summary>
        ///     Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            this.Stop();
        }
        #endregion
    }
}